Add ASAN and Valgrind integration for CI test suite#289
Add ASAN and Valgrind integration for CI test suite#289irodushka merged 18 commits intoFirebirdSQL:masterfrom
Conversation
On Linux, sizeof(wchar_t) = 4 bytes but sizeof(SQLWCHAR) = 2 bytes
(unixODBC defines SQLWCHAR as unsigned short). The ConvertingString
constructor used sizeof(wchar_t) to convert a byte-count argument
into the number of narrow characters needed:
lengthString = length / sizeof(wchar_t); // = 12/4 = 3 on Linux
For SQLGetDiagRecW the state buffer is declared as State(12, sqlState),
giving lengthString=3 and Alloc() allocating 3+2=5 bytes. strcpy()
then writes the 5-character SQL state plus its NUL terminator (6 bytes)
into that 5-byte buffer, producing a 1-byte heap-buffer-overflow caught
by AddressSanitizer.
The same latent bug exists in SQLErrorW (same State(12, sqlState) pattern).
Fix: divide by sizeof(SQLWCHAR) instead of sizeof(wchar_t).
sizeof(SQLWCHAR) == 2 on all platforms (Windows: SQLWCHAR=wchar_t=2;
Linux/unixODBC: SQLWCHAR=unsigned short=2), so the formula now yields:
lengthString = 12 / sizeof(SQLWCHAR) = 6
and Alloc() allocates 6+2=8 bytes, comfortably holding the SQL state.
On Windows sizeof(wchar_t)==sizeof(SQLWCHAR)==2, so this change is
a no-op there.
Found by: AddressSanitizer (introduced in CI via PR FirebirdSQL#288/FirebirdSQL#289)
Test: DataTypeTest.SmallintRoundTrip -> ExecIgnoreError -> SQLExecDirect
-> unixODBC dispatcher -> SQLGetDiagRecW -> sqlGetDiagRec(strcpy)
Bug found and fixed by ASANThe ASAN CI job introduced by this PR immediately caught a real memory bug in the driver. What ASAN reportedThe first ASAN run failed on The full output is in the CI job log: GitHub Actions -> Build and Test -> job Root cause
ConvertingString<> State( 12, sqlState );In lengthString = length / sizeof(wchar_t); // BUGOn Windows
The bug was silently harmless on Windows and went undetected for years. FixOne-line change in // Before
lengthString = length / sizeof(wchar_t);
// After
lengthString = length / sizeof(SQLWCHAR);
CI after fixAll 4 matrix jobs now pass with no
This is a concrete demonstration that ASAN integration pays off immediately. |
|
@irodushka Enjoy! 😉 |
|
Hi @fdcastel Can you please resolve the conflicts after merging PR#286 Cheers! |
Sure! I’ll look into this in a few hours. |
Add CMake options ENABLE_ASAN and ENABLE_VALGRIND (Linux/GCC/Clang only, mutually exclusive) with corresponding compiler/linker flags and CTest memcheck configuration. CMake changes: - ENABLE_ASAN appends -fsanitize=address -fno-omit-frame-pointer to compile and link flags; suppresses -DLOGGING in Debug builds for cleaner sanitizer output - ENABLE_VALGRIND finds the valgrind binary and configures MEMORYCHECK_COMMAND for ctest -T memcheck - Mutual exclusion enforced via FATAL_ERROR CMake presets: - Add 'asan' and 'valgrind' configure/build/test presets inheriting from 'debug', with ASAN_OPTIONS env and Valgrind timeout multiplier Build script (firebird-odbc-driver.build.ps1): - Add -Sanitizer parameter (None, Asan, Valgrind) that passes the corresponding CMake option; sets ASAN_OPTIONS at runtime; skips sanitizers on Windows with a warning CI (build-and-test.yml): - Add Linux x64 ASAN matrix entry (ubuntu-22.04, Debug) - Add Linux x64 Valgrind matrix entry (ubuntu-22.04, Debug) with valgrind package installation - Sanitizer jobs do not upload release artifacts Also adds valgrind.supp suppressions file (seeded with fbclient rule). Closes FirebirdSQL#288
LSAN_OPTIONS referenced a relative 'lsan.supp' path but CTest runs from the build directory. Use an absolute path derived from the source/workspace root so the file is always found. Also adds the lsan.supp suppressions file with initial rules for libfbclient, libodbc, and libodbcinst.
ASAN correctly detected a pre-existing heap-buffer-overflow in OdbcError::sqlGetDiagRec (strcpy into an undersized ConvertingString buffer). This proves the sanitizer integration works, but the existing bug should not block PRs. Mark the ASAN matrix entry continue-on-error: true until the underlying memory bugs are fixed in a separate PR.
On Linux, sizeof(wchar_t) = 4 bytes but sizeof(SQLWCHAR) = 2 bytes
(unixODBC defines SQLWCHAR as unsigned short). The ConvertingString
constructor used sizeof(wchar_t) to convert a byte-count argument
into the number of narrow characters needed:
lengthString = length / sizeof(wchar_t); // = 12/4 = 3 on Linux
For SQLGetDiagRecW the state buffer is declared as State(12, sqlState),
giving lengthString=3 and Alloc() allocating 3+2=5 bytes. strcpy()
then writes the 5-character SQL state plus its NUL terminator (6 bytes)
into that 5-byte buffer, producing a 1-byte heap-buffer-overflow caught
by AddressSanitizer.
The same latent bug exists in SQLErrorW (same State(12, sqlState) pattern).
Fix: divide by sizeof(SQLWCHAR) instead of sizeof(wchar_t).
sizeof(SQLWCHAR) == 2 on all platforms (Windows: SQLWCHAR=wchar_t=2;
Linux/unixODBC: SQLWCHAR=unsigned short=2), so the formula now yields:
lengthString = 12 / sizeof(SQLWCHAR) = 6
and Alloc() allocates 6+2=8 bytes, comfortably holding the SQL state.
On Windows sizeof(wchar_t)==sizeof(SQLWCHAR)==2, so this change is
a no-op there.
Found by: AddressSanitizer (introduced in CI via PR FirebirdSQL#288/FirebirdSQL#289)
Test: DataTypeTest.SmallintRoundTrip -> ExecIgnoreError -> SQLExecDirect
-> unixODBC dispatcher -> SQLGetDiagRecW -> sqlGetDiagRec(strcpy)
The heap-buffer-overflow in ConvertingString (sizeof(wchar_t) vs sizeof(SQLWCHAR)) has been fixed. ASAN now passes all 375 tests cleanly on ubuntu-22.04/GCC, so the soft-fail guard is no longer needed.
|
@irodushka Rebased onto current Conflicts were in Resolution: kept all new upstream matrix entries and added the sanitizer entries on top; merged the |
- Move option() declarations outside if(NOT MSVC) guard; emit warning on MSVC instead of hiding the options entirely - Rename ENABLE_ASAN -> BUILD_WITH_ASAN and ENABLE_VALGRIND -> BUILD_WITH_VALGRIND to follow existing BUILD_* naming convention - Remove duplicate target_compile_definitions for DEBUG/LOGGING (already defined via add_compile_options in Linux section) - Separate LOGGING into add_definitions() and conditionally skip it for both ASAN and Valgrind builds (not just ASAN)
Per PR review feedback: consolidate the build-type options matrix and express the sanitizer carve-out via add_definitions/remove_definitions instead of an inverted if-NOT guard. Behavior is unchanged — LOGGING is defined for Debug builds and stripped for ASAN/Valgrind builds.
|
@irodushka Thanks for the valuable insights. I’ve just pushed all the changes. Please take a look when you have a moment. |
Per review feedback: remove_definitions() cannot undo definitions added via add_compile_options(), so the add-then-remove pattern was misleading. Collapse to a single if() with the combined condition instead.
|
We have a bug! The reason is in the Can you please remind me what do you expect specifying the |
The first git describe call used --always, which made it return the short commit hash instead of failing when --exclude "*-*" ruled out every matching tag (as is the case today: only prerelease tags like v3.5.0-rc5 exist). The non-zero exit that was supposed to trigger the fallback without --exclude never fired, so parsing dropped to 0.0.0.0. Drop --always from both calls: if the stable-only call finds nothing it exits non-zero and the fallback runs; if the fallback also finds nothing the existing warning path reports it.
|
@irodushka you're right, good catch. Fixed in b7aa955. Intent of the logic
Why it was broken
The fix
Verified locally: |
|
Aha. Great) Another note - let's exclude |
|
Bad thing happens. is not good. I get while running So it's not such a trivial case with that bug. I will investigate tomorrow for it's a bit late here now. But you understand, I'm holding off on this PR until we solve this mystery. |
Okay, I'm in no hurry) Just one thing to say - I looked into PR #283 and, frankly, I don't like what I see in the MainUnicode.cpp and in the reworked |
🤣
That was just the first attempt 😉 -- no worries! SURE, We will fix all that. |
|
Reverted back to |
|
P.S: Added a minimum-size floor in internal This closes the original heap-buffer-overflow without touching the |
Follow-up to the ConvertingString discussion in FirebirdSQL#289. The 8-byte floor in Alloc() introduced by the previous commit was a crutch: it avoided the strcpy heap overflow for the SQL-state case but left the destructor's mbstowcs((wchar_t*)unicodeString, ..., lengthString) writing 4-byte wchar_t units into a caller buffer that is SQLWCHAR-sized (2 bytes). Even when that did not overflow, the data was wrong — 'HY000' became 'H' after the client reinterpreted the bytes as UCS-2. Replace the Linux non-connection paths in both directions with the same byte-widening / byte-narrowing loop that unixODBC itself uses internally (ansi_to_unicode_copy / unicode_to_ansi_copy). This is correct for the ASCII-only error/state strings that reach this code; non-ASCII handling remains the subject of the broader rewrite tracked as Tier 9.1 in FirebirdSQL#287. Changes in MainUnicode.cpp: - lengthString now uses sizeof(SQLWCHAR) again (correct SQLWCHAR count). - Destructor Linux branch widens bytes into SQLWCHAR units. - convUnicodeToString Linux branch narrows SQLWCHAR to low bytes. - Temporary NUL is written as a SQLWCHAR-sized zero (not wchar_t). - Remove the Alloc() floor — no longer needed. - Add sqlwcharLen() helper; wcslen() on SQLWCHAR data is unsafe on Linux.
|
I really appreciate your work, but I think we're taking a wrong turn.
|
|
So. What about following the plan written below?)
|
|
Updated #287: Moved Tasks from Tier 9.1 to Tier 1b. I’ll be working on the split PRs over the next few hours. |
The primary task (after the asan/valgrind PR cleaning) is to develop the widechar odbc tests. Okay? |
Yep. Loud and clear! 😉 |
Revert the MainUnicode.cpp changes (832d8e7, 0f8f90f, bea6403) per irodushka's plan in FirebirdSQL#289 comment 4279111183 — the Unicode rewrite is moving to its own PR, preceded by a widechar-tests PR. Restore continue-on-error on the ASAN matrix job temporarily; Valgrind stays strict. ASAN will flag the heap-buffer-overflow in SQLGetDiagRecW again; this is the known issue the follow-up PRs will fix and is tracked as issue FirebirdSQL#287 Tier 1b.
|
@irodushka — split done per your plan:
|
|
Just a reminder: these are items that were intentionally left out of scope for this plan/PR.
All of these belong in #287 (Tier 1b). After #291 has landed. |
|
P.S.: As expected, ASAN is failing now. It will be green after #291, too. 😉 |
|
I think we should remove the |
…ectW / SQLPrepareW) Exercises both ConvertingString constructor paths in MainUnicode.cpp: output buffer (SQLGetDiagRecW / SQLErrorW) and input buffer (SQLExecDirectW / SQLPrepareW). Linux tests skip with a pointer to the follow-up Unicode rewrite PR; they are meant to drive that PR's implementation. Windows runs them for real. Per the plan in #289 comment 4279111183. Tracked as issue #287 Tier 1b.
LPWSTR is WCHAR* = unsigned short* = SQLWCHAR* on both Windows and unixODBC/Linux, so `*(LPWSTR)ptr = L'\0'` writes 2 bytes via narrowing store, not 4 — no overrun. Thanks to @irodushka for the correction (FirebirdSQL#289 discussion_r3109004999). Keeping the replacement `unicodeString[len] = 0;` which he endorsed as more compact and understandable. The sibling comment in convUnicodeToString stays — that one is a real 4-byte write through an explicit `(wchar_t*)` cast.
Per FirebirdSQL#289 discussion with @irodushka: rather than keep ASAN running under continue-on-error, disable it outright until the Unicode rewrite lands (draft PR FirebirdSQL#291, tracked as FirebirdSQL#287 Tier 1b). Re-enable the matrix entry once FirebirdSQL#291 merges. Valgrind remains strict.
|
Agreed. Done in 98115a1:
Leaving the merge to you 😉. |
|
I need a beer! 🤣 P.S; @irodushka Thank you very much for your patience and guidance. Much appreciated! 🤗 |
Come to Moscow)) |
Second invite in less than a month. Adding it to my ever-growing “dangerously tempting ideas” list. 😅 |

Summary
Integrate AddressSanitizer (ASAN) and Valgrind into the CI test pipeline so that memory bugs (buffer overflows, use-after-free, leaks, etc.) are caught automatically on every PR.
Closes #288
Changes
CMake (
CMakeLists.txt)ENABLE_ASANoption (default OFF, Linux/GCC/Clang only): appends-fsanitize=address -fno-omit-frame-pointerto compile and link flags. Suppresses-DLOGGINGin Debug builds for cleaner sanitizer output.ENABLE_VALGRINDoption (default OFF): finds thevalgrindbinary and configuresMEMORYCHECK_COMMAND/MEMORYCHECK_COMMAND_OPTIONSforctest -T memcheck.FATAL_ERROR.if(NOT MSVC)since MSVC ASAN has different semantics (future work).CMake Presets (
CMakePresets.json)asanconfigure/build/test presets inheriting fromdebug, withASAN_OPTIONSenvironment variable.valgrindconfigure/build/test presets inheriting fromdebug, with a 600s test timeout (Valgrind is ~10-20x slower).Build script (
firebird-odbc-driver.build.ps1)-Sanitizerparameter acceptingNone,Asan,Valgrind.-DENABLE_ASAN=ONor-DENABLE_VALGRIND=ONto CMake.ASAN_OPTIONS/LSAN_OPTIONSenvironment variables at runtime for ASAN builds.CI (
build-and-test.yml)valgrindpackage installation.ASAN_OPTIONSandLSAN_OPTIONSenvironment variables.Valgrind suppressions (
valgrind.supp)libfbclient.soglobal initialization leaks.Local usage